// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © fluxchart

//@version=5
const bool DEBUG = false
const bool DEBUGOBFVG = false
const int maxBoxesCount = 500
const int maxDistanceToLastBar = 3000 // Affects Running Time
const int maxOrderBlocks = 60
const int showLastXFVGs = 20
const int extendLastXFVGsCount = 20
const int minimumFVGSize = 2
const int minimumIFVGSize = 2
const float overlapThresholdPercentage = 0
const int atrLen = 10

indicator("ICT Unicorn | Flux Charts", overlay = true, max_boxes_count = maxBoxesCount, max_labels_count = maxBoxesCount, max_lines_count = maxBoxesCount)

//#region Settings
fvgSensitivityText = input.string("Normal", "FVG Detection Sensitivity", options = ["Extreme", "High", "Normal", "Low"], group = "General Configuration")
swingLength = input.int(10, 'Swing Length', minval = 3, tooltip = "Swing length is used when finding order block formations. Smaller values will result in finding smaller order blocks.", group = "General Configuration", display = display.none)
dbgRequireRetracement = input.bool(false, "Require Retracement", tooltip = "A retracement to the FVG will be required for entry confirmation if enabled.", group = "General Configuration")
showBB = input.bool(true, "Show Breaker Blocks", inline = "bb", group = "General Configuration")
showFVG = input.bool(true, "FVGs", inline = "bb", group = "General Configuration")

entryWaitBars = DEBUG ? input.int(1, "[DBG] Entry Wait Bars", group = "General Configuration") : 1
dbgLabelSize = DEBUG ? input.string("Normal", "[DBG] Label Size", ["Normal", "Small", "Tiny"], group = "General Configuration") : "Normal"
lblSize = (dbgLabelSize == "Small" ? size.small : dbgLabelSize == "Normal" ? size.normal : size.tiny)
unicornTPSLLength = DEBUG ? input.int(100, "[DBG] Unicorn TP / SL Length", group = "General Configuration") : 100
dbgShowBBFVG = DEBUG ? input.bool(false, "[DBG] Show All BB and FVGs", group = "General Configuration") : false

showTPSL = input.bool(true, "Enabled", group = "TP / SL")
tpslMethod = input.string("Unicorn", "TP / SL Method", options = ["Unicorn", "Dynamic", "Fixed"], group = "TP / SL")
riskAmount = input.string("Normal", "Dynamic Risk", options = ["Highest", "High", "Normal", "Low", "Lowest"], group = "TP / SL", tooltip = "The risk amount when Dynamic TP / SL method is selected.\n\nDifferent assets may have different volatility so changing this setting may result in change of performance of the indicator.")
customSLATRMult = DEBUG ? input.float(6.5, "[DBG] Dynamic Custom Risk Mult", group = "TP / SL") : 6.5
dbgUnicornSLOffset = DEBUG ? input.float(4.75, "[DBG] Unicorn SL Offset", group = "TP / SL") : 4.75

slATRMult = riskAmount == "Highest" ? 9.5 : riskAmount == "High" ? 6 : riskAmount == "Normal" ? 5 : riskAmount == "Low" ? 4 : riskAmount == "Lowest" ? 1.5 : customSLATRMult
tpPercent = input.float(0.3, "Fixed Take Profit %", group = "TP / SL")
slPercent = input.float(0.4, "Fixed Stop Loss %", group = "TP / SL")

backtestDisplayEnabled = input.bool(true, "Enabled", group = "Backtesting Dashboard", display = display.none)
backtestingLocation = input.string("Top Center", "Position", options = ["Top Right", "Right Center", "Top Center"], group = "Backtesting Dashboard", display = display.none)
fillBackgrounds = input.bool(true, "Fill Backgrounds", group = "Backtesting Dashboard", display = display.none)
screenerColor = input.color(#1B1F2B, 'Background', inline = "1", group = 'Backtesting Dashboard', display = display.none)

UnicornRR = DEBUG ? input.float(0.57, "Unicorn Risk:Reward Ratio", group = "Debug") : 0.57
DynamicRR = DEBUG ? input.float(0.86, "Dynamic Risk:Reward Ratio", group = "Debug") : 0.86

buyAlertEnabled = input.bool(true, "Buy Signal", inline = "BS", group = "Alerts")
sellAlertEnabled = input.bool(true, "Sell Signal", inline = "BS", group = "Alerts")
tpAlertEnabled = input.bool(true, "Take-Profit Signal", inline = "TS", group = "Alerts")
slAlertEnabled = input.bool(true, "Stop-Loss Signal ", inline = "TS", group = "Alerts")

dbgTPSLVersion = input.string("Default", "TP / SL Layout", options = ["Default", "Alternative"], group = "Visuals")

bullishBreakerBlockColor = input(color.new(#2962ff, 75), "Bullish Breaker", inline = 'breakerColor', group = 'Visuals', display = display.none)
bearishBreakerBlockColor = input(color.new(#ffeb3b, 75), "Bearish Breaker", inline = 'breakerColor', group = 'Visuals', display = display.none)

highColor = input.color(#08998180, "Buy", inline = "colors", group = "Visuals")
lowColor = input.color(#f2364680, "Sell", inline = "colors", group = "Visuals")
textColor = input.color(#ffffff, "Text", inline = "colors", group = "Visuals")

showInvalidated = DEBUGOBFVG ? input.bool(true, "Show Historic Zones", group = "General Configuration", display = display.none) : true

//#region FVG Settings
fvgEnabled = DEBUGOBFVG ? input.bool(true, "Enabled", group = "Fair Value Gaps", inline="EV", display = display.none) : true
fvgVolumetricInfo = DEBUGOBFVG ? input.bool(false, "Volumetric Info", group = "Fair Value Gaps", inline="EV", display = display.none) : false
fvgEndMethod = DEBUGOBFVG ? input.string("Close", "Zone Invalidation", options = ["Wick", "Close"],  group = "Fair Value Gaps") : "Close"
fvgFilterMethod = DEBUGOBFVG ? input.string("Average Range", "Zone Filtering", options = ["Average Range", "Volume Threshold"],  group = "Fair Value Gaps") : "Average Range"
volumeThresholdPercent = DEBUGOBFVG ? input.int(50, "Volume Threshold %", group = "Fair Value Gaps", tooltip = "Only taken into calculation when filter method is Volume Threshold.", minval = 1, maxval = 200) : 50
fvgBars = DEBUGOBFVG ? input.string("Same Type", "FVG Detection", options = ["Same Type", "All"], tooltip = "Same Type -> All 3 bars that formed the FVG should be the same type. (Bullish / Bearish) \n\nAll -> Bar types may vary between bullish / bearish.", group = "Fair Value Gaps") : "Same Type"
fvgSensEnabled = DEBUGOBFVG ? input.bool(true, "", "", "sens", "Fair Value Gaps") : true
//fvgSensitivityText = DEBUGOBFVG ? input.string("Extreme", "Detection Sensitivity", inline = "sens", options = ["Extreme", "High", "Normal", "Low"], group = "Fair Value Gaps") : "Extreme"
fvgBullColor = DEBUGOBFVG ? input(#08998180, 'Bullish', inline = 'fvgColor', group = 'Fair Value Gaps', display = display.none) : #08998180
fvgBearColor = DEBUGOBFVG ? input(#f2364680, 'Bearish', inline = 'fvgColor', group = 'Fair Value Gaps', display = display.none) : #f2364680
combineFVGs = DEBUGOBFVG ? input.bool(false, "Combine Zones", group = "Fair Value Gaps") : false
allowGaps = DEBUGOBFVG ? input.bool(false, "Allow Gaps Between Bars", group = "Fair Value Gaps", tooltip = "On tickers that can have a different opening price than the previous bar's closing price, the indicator will not analyze those bars for FVGs if disabled.\n\nFor Example, if the today's opening price is different from the yesterday's closing price in a stock ticker.") : false
deleteUntouched = DEBUGOBFVG ? input.bool(true, "", group = "Fair Value Gaps", inline = "deleteUntouched") : true
deleteUntouchedAfterXBars = DEBUGOBFVG ? input.int(200, "Delete Untouched Zones After", group = "Fair Value Gaps", minval = 5, maxval = 200, inline = "deleteUntouched") : 200

ifvgEnabled = DEBUGOBFVG ? input.bool(false, "Enabled", inline="EV", group = "Inversion Fair Value Gaps") : false
ifvgVolumetricInfo = DEBUGOBFVG ? input.bool(false, "Volumetric Info", group = "Inversion Fair Value Gaps", inline="EV", display = display.none) : false
ifvgEndMethod = DEBUGOBFVG ? input.string("Wick", "IFVG Zone Invalidation", options = ["Wick", "Close"],  group = "Inversion Fair Value Gaps") : "Wick"
ifvgFull = DEBUGOBFVG ? input.bool(true, "IFVG Full", group = "Inversion Fair Value Gaps", display = display.none) : true
bullishInverseColor = DEBUGOBFVG ? input(#08998180, "Bullish", inline = 'breakerColor', group = 'Inversion Fair Value Gaps', display = display.none) : #08998180
bearishInverseColor = DEBUGOBFVG ? input(#f2364580, "Bearish", inline = 'breakerColor', group = 'Inversion Fair Value Gaps', display = display.none) : #f2364580
//#endregion

//#region OB Settings
OBsEnabled = DEBUGOBFVG ? input.bool(false, "Enabled", group = "Order Blocks", inline="EV", display = display.none) : false
orderBlockVolumetricInfo = DEBUGOBFVG ? input.bool(true, "Volumetric Info", group = "Order Blocks", inline="EV", display = display.none) : true
obEndMethod = DEBUGOBFVG ? input.string("Close", "Zone Invalidation", options = ["Wick", "Close"],  group = "Order Blocks", display = display.none) : "Close"
combineOBs = DEBUGOBFVG ? input.bool(false, "Combine Zones", group = "Order Blocks", display = display.none) : false
//swingLength = DEBUGOBFVG ? input.int(10, 'Swing Length', minval = 3, tooltip="Swing length is used when finding order block formations. Smaller values will result in finding smaller order blocks.",group = "Order Blocks", display = display.none) : 10
zoneCountOB = DEBUGOBFVG ? input.string("High", 'Zone Count', options = ["High", "Medium", "Low"], tooltip = "Number of Order & Breaker Block Zones to be rendered. Higher options will result in older Order & Breaker Blocks shown.",  group = "Order Blocks", display = display.none) : "High"
bullOrderBlockColor = DEBUGOBFVG ? input(#08998180, 'Bullish', inline = 'obColor', group = 'Order Blocks', display = display.none) : #08998180
bearOrderBlockColor = DEBUGOBFVG ? input(#f2364680, 'Bearish', inline = 'obColor', group = 'Order Blocks', display = display.none) : #f2364680

BBsEnabled = DEBUGOBFVG ? input.bool(true, "Enabled", group = "Breaker Blocks", inline="EV", display = display.none) : true
breakBlockVolumetricInfo = DEBUGOBFVG ? input.bool(false, "Volumetric Info", group = "Breaker Blocks", inline="EV", display = display.none) : false
bbEndMethod = DEBUGOBFVG ? input.string("Close", "Zone Invalidation", options = ["Wick", "Close"],  group = "Breaker Blocks", display = display.none) : "Close"
breakersFull = DEBUGOBFVG ? input.bool(true, "Breakers Full", group = "Breaker Blocks", display = display.none) : true
//#endregion

//#region DEBUG Settings
maxATRMult = (DEBUG and DEBUGOBFVG) ? input.float(3.5,"Max ATR Multiplier", group = "General Configuration", display = display.none) : 3.5
extendZonesBy = (DEBUG and DEBUGOBFVG) ? input.int(15, "Extend Zones", group = "Style", minval = 1, maxval = 30, inline = "ExtendZones") : 15
extendZonesDynamic = (DEBUG and DEBUGOBFVG) ? input.bool(true, "Dynamic", group = "Style", inline = "ExtendZones") : true
extendLastFVGs = (DEBUG and DEBUGOBFVG) ? input.bool(true, "Extend Last Zones", group = "Style") : true
changeCombinedFVGsColor = (DEBUG and DEBUGOBFVG) ? input.bool(false, "Change Combined Zones Color", group = "Style", inline = "CombinedColor") : false
combinedText = (DEBUG and DEBUGOBFVG) ? input.bool(false, "Combined Text", group = "Style", inline = "CombinedColor") : false
combinedColor = (DEBUG and DEBUGOBFVG) ? input.color(#fff70080, DEBUG ? "" : "Combined Zone Color", group = "Style", inline = "CombinedColor") : #fff70080
startZoneFrom = (DEBUG and DEBUGOBFVG) ? input.string("Last Bar", "Start FVG Zones From", options = ["First Bar", "Last Bar"], group = "Style"): "Last Bar"
volumeBarsPlace = (DEBUG and DEBUGOBFVG) ? input.string("Left", "Show Volume Bars At", options = ["Left", "Right"], group = "Style", inline = "volumebars") : "Left"
mirrorVolumeBars = (DEBUG and DEBUGOBFVG) ? input.bool(true, "Mirror Volume Bars", group = "Style", inline = "volumebars") : true
//#endregion
//#endregion

//#region UDTs
type FVGInfo
    float max = na
    float min = na
    bool isBull = na
    int t = na
    float totalVolume = na
    int startBarIndex = na
    int endBarIndex = na
    int startTime = na
    int endTime = na
    bool extendInfinite = false
    bool combined = false
    string combinedTimeframesStr = na
    bool disabled = false
    string timeframeStr = na

    float lowVolume = na
    float highVolume = na
    bool isInverse = false
    int lastTouched = na
    int lastTouchedIFVG = na
    int inverseEndIndex = na
    int inverseEndTime = na
    float inverseVolume

createFVGInfo (h,l,bull,t,tv) =>
    FVGInfo newFVGInfo = FVGInfo.new(h, l, bull, t, tv)
    newFVGInfo

type FVG
    FVGInfo info = na
    
    bool isRendered = false

    box fvgBox = na
    box ifvgBox = na
    box fvgBoxText = na
    box fvgBoxPositive = na
    box fvgBoxNegative = na

    line fvgSeperator = na
    line fvgTextSeperator = na

createFVG (FVGInfo FVGInfoF) =>
    FVG newFVG = FVG.new(FVGInfoF)
    newFVG

safeDeleteFVG (FVG fvg) =>
    fvg.isRendered := false

    box.delete(fvg.fvgBox)
    box.delete(fvg.ifvgBox)
    box.delete(fvg.fvgBoxText)
    box.delete(fvg.fvgBoxPositive)
    box.delete(fvg.fvgBoxNegative)

    line.delete(fvg.fvgSeperator)
    line.delete(fvg.fvgTextSeperator)

type orderBlockInfo
    float top
    float bottom
    float obVolume
    string obType
    int startTime
    float bbVolume
    float obLowVolume
    float obHighVolume
    bool breaker = false
    int breakTime
    string timeframeStr
    bool disabled = false
    string combinedTimeframesStr = na
    bool combined = false

type orderBlock
    orderBlockInfo info
    bool isRendered = false

    box orderBox = na
    box breakerBox = na

    line orderBoxLineTop = na
    line orderBoxLineBottom = na
    line breakerBoxLineTop = na
    line breakerBoxLineBottom = na
    //
    box orderBoxText = na
    box orderBoxPositive = na
    box orderBoxNegative = na

    line orderSeperator = na
    line orderTextSeperator = na

createOrderBlock (orderBlockInfo orderBlockInfoF) =>
    orderBlock newOrderBlock = orderBlock.new(orderBlockInfoF)
    newOrderBlock

safeDeleteOrderBlock (orderBlock orderBlockF) =>
    orderBlockF.isRendered := false

    box.delete(orderBlockF.orderBox)
    box.delete(orderBlockF.breakerBox)
    box.delete(orderBlockF.orderBoxText)
    box.delete(orderBlockF.orderBoxPositive)
    box.delete(orderBlockF.orderBoxNegative)

    line.delete(orderBlockF.orderBoxLineTop)
    line.delete(orderBlockF.orderBoxLineBottom)
    line.delete(orderBlockF.breakerBoxLineTop)
    line.delete(orderBlockF.breakerBoxLineBottom)
    line.delete(orderBlockF.orderSeperator)
    line.delete(orderBlockF.orderTextSeperator)

type timeframeInfo
    int index = na
    string timeframeStr = na
    bool isEnabled = false

    FVGInfo[] FVGInfoList = na
    orderBlockInfo[] bullishOrderBlocksList = na
    orderBlockInfo[] bearishOrderBlocksList = na

newTimeframeInfo (index, timeframeStr, isEnabled) =>
    newTFInfo = timeframeInfo.new()
    newTFInfo.index := index
    newTFInfo.isEnabled := isEnabled
    newTFInfo.timeframeStr := timeframeStr

    newTFInfo.FVGInfoList := array.new<FVGInfo>(0)
    newTFInfo.bullishOrderBlocksList := array.new<orderBlockInfo>(0)
    newTFInfo.bearishOrderBlocksList := array.new<orderBlockInfo>(0)

    newTFInfo

type obSwing
    int x = na    
    float y = na
    float swingVolume = na
    bool crossed = false
//#endregion

//#region Definitions

volumeBarsLeftSide = (volumeBarsPlace == "Left")
fvgSensitivity = fvgSensitivityText == "Extreme" ? 6 : fvgSensitivityText == "High" ? 2 : fvgSensitivityText == "Normal" ? 1.5 : 1
extendZonesByTime = extendZonesBy * timeframe.in_seconds(timeframe.period) * 1000

bullishOrderBlocks = zoneCountOB == "Low" ? 3 : zoneCountOB == "Medium" ? 5 : 60
bearishOrderBlocks = zoneCountOB == "Low" ? 3 : zoneCountOB == "Medium" ? 5 : 60

atr = ta.atr(atrLen)
volCheck = (ta.cum(volume) > 0)

moveLine(_line, _x, _y, _x2) =>
    line.set_xy1(_line, _x,  _y)
    line.set_xy2(_line, _x2, _y)

moveBox (_box, _topLeftX, _topLeftY, _bottomRightX, _bottomRightY) =>
    box.set_lefttop(_box, _topLeftX, _topLeftY)
    box.set_rightbottom(_box, _bottomRightX, _bottomRightY)

isTimeframeLower (timeframe1F, timeframe2F) =>
    timeframe.in_seconds(timeframe1F) < timeframe.in_seconds(timeframe2F)

getMinTimeframe (timeframe1F, timeframe2F) =>
    if isTimeframeLower(timeframe1F, timeframe2F)
        timeframe1F
    else
        timeframe2F

getMaxTimeframe (timeframe1F, timeframe2F) =>
    if isTimeframeLower(timeframe1F, timeframe2F)
        timeframe2F
    else
        timeframe1F

formatTimeframeString (formatTimeframe) =>
    timeframeF = formatTimeframe == "" ? timeframe.period : formatTimeframe
    
    if str.contains(timeframeF, "D") or str.contains(timeframeF, "W") or str.contains(timeframeF, "S") or str.contains(timeframeF, "M")
        timeframeF
    else
        seconds = timeframe.in_seconds(timeframeF)
        if seconds >= 3600
            hourCount = int(seconds / 3600)
            str.tostring(hourCount) + " Hour" + (hourCount > 1 ? "s" : "")
        else
            timeframeF + " Min"

betterCross(s1, s2) =>
    string ret = na
    if s1 >= s2 and s1[1] < s2
        ret := "Bull"
    if s1 < s2 and s1[1] >= s2
        ret := "Bear"
    ret

colorWithTransparency (colorF, transparencyX) =>
    color.new(colorF, color.t(colorF) * transparencyX)

createFVGBox (boxColor, transparencyX = 1.0, xlocType = xloc.bar_time) =>
    box.new(na, na, na, na, text_size = size.normal, xloc = xlocType, extend = extend.none, bgcolor = colorWithTransparency(boxColor, transparencyX), text_color = textColor, text_halign = text.align_center, border_color = #00000000)

var timeframeInfo[] timeframeInfos = array.from(newTimeframeInfo(1, "", true))
var FVGInfo[] FVGInfoList = array.new<FVGInfo>(0)
var bullishOrderBlocksList = array.new<orderBlockInfo>(0)
var bearishOrderBlocksList = array.new<orderBlockInfo>(0)

var allOrderBlocksList = array.new<orderBlock>(0)
var FVG[] allFVGList = array.new<FVG>(0)
//#endregion

//#region Order Blocks
renderOrderBlock (orderBlock ob, int customEndTime) =>
    orderBlockInfo info = ob.info
    ob.isRendered := true
    orderColor = ob.info.obType == "Bull" ? bullOrderBlockColor : bearOrderBlockColor
    breakerBlockColor = ob.info.obType == "Bull" ? bearishBreakerBlockColor : bullishBreakerBlockColor
    if OBsEnabled and (not breakersFull or not (BBsEnabled and info.breaker)) and not (not showInvalidated and info.breaker)
        ob.orderBox := createFVGBox(orderColor, 1.5)
        if ob.info.combined and not changeCombinedFVGsColor
            ob.orderBox.set_bgcolor(colorWithTransparency(orderColor, 1.1))
        ob.orderBoxText := createFVGBox(color.new(color.white, 100))
        if orderBlockVolumetricInfo
            ob.orderBoxPositive := createFVGBox(bullOrderBlockColor)
            ob.orderBoxNegative := createFVGBox(bearOrderBlockColor)
            ob.orderSeperator := line.new(na,na,na,na,xloc.bar_time,extend.none,textColor,line.style_dashed,1)
            ob.orderTextSeperator := line.new(na,na,na,na,xloc.bar_time,extend.none,textColor,line.style_solid,1)

        zoneSize = extendZonesDynamic ? na(info.breakTime) ? extendZonesByTime : (info.breakTime - info.startTime) : extendZonesByTime
        if na(info.breakTime)
            zoneSize := (time + 1) - info.startTime

        startX = volumeBarsLeftSide ? info.startTime : info.startTime + zoneSize - zoneSize / 3
        maxEndX = volumeBarsLeftSide ? info.startTime + zoneSize / 3 : info.startTime + zoneSize

        moveBox(ob.orderBox, info.startTime, info.top, info.startTime + zoneSize, info.bottom)
        moveBox(ob.orderBoxText, volumeBarsLeftSide ? maxEndX : info.startTime, info.top, volumeBarsLeftSide ? info.startTime + zoneSize : startX, info.bottom)

        percentage = int((math.min(info.obHighVolume, info.obLowVolume) / math.max(info.obHighVolume, info.obLowVolume)) * 100.0)
        //OBText = (na(ob.info.combinedTimeframesStr) ? formatTimeframeString(ob.info.timeframeStr) : ob.info.combinedTimeframesStr) + " OB"
        OBText = ""
        box.set_text(ob.orderBoxText, (orderBlockVolumetricInfo ? str.tostring(ob.info.obVolume, format.volume) + " (" + str.tostring(percentage) + "%)\n" : "") + (combinedText and ob.info.combined ? "[Combined]\n" : "") + OBText)

        if orderBlockVolumetricInfo
            showHighLowBoxText = false

            curEndXHigh = int(math.ceil((info.obHighVolume / info.obVolume) * (maxEndX - startX) + startX))
            curEndXLow = int(math.ceil((info.obLowVolume / info.obVolume) * (maxEndX - startX) + startX))

            moveBox(ob.orderBoxPositive, mirrorVolumeBars ? startX : curEndXLow, info.top, mirrorVolumeBars ? curEndXHigh : maxEndX, (info.bottom + info.top) / 2)
            box.set_text(ob.orderBoxPositive, showHighLowBoxText ? str.tostring(info.obHighVolume, format.volume) : "")

            moveBox(ob.orderBoxNegative, mirrorVolumeBars ? startX : curEndXHigh, info.bottom, mirrorVolumeBars ? curEndXLow : maxEndX, (info.bottom + info.top) / 2)
            box.set_text(ob.orderBoxNegative, showHighLowBoxText ? str.tostring(info.obLowVolume, format.volume) : "")

            moveLine(ob.orderSeperator, volumeBarsLeftSide ? startX : maxEndX, (info.bottom + info.top) / 2, volumeBarsLeftSide ? maxEndX : startX)

            line.set_xy1(ob.orderTextSeperator, volumeBarsLeftSide ? maxEndX : startX, info.top)
            line.set_xy2(ob.orderTextSeperator, volumeBarsLeftSide ? maxEndX : startX, info.bottom)

    if info.breaker and BBsEnabled
        startTime = (OBsEnabled and not breakersFull) ? info.breakTime : info.startTime
        endTime = time + 1
        if not na(customEndTime)
            endTime := customEndTime
        
        ob.breakerBox := box.new(startTime, info.top, endTime, info.bottom, na, bgcolor = breakerBlockColor, extend = extend.none, xloc = xloc.bar_time, text_color = textColor, text_size = size.normal)
        //BBText = (na(ob.info.combinedTimeframesStr) ? formatTimeframeString(ob.info.timeframeStr) : ob.info.combinedTimeframesStr) + " BB"
        BBText = ""
        box.set_text(ob.breakerBox, (breakBlockVolumetricInfo ? str.tostring(ob.info.bbVolume, format.volume) + "\n" : "") + (combinedText and ob.info.combined ? "[Combined]\n" : "") + BBText)
        ob.breakerBoxLineTop := line.new(startTime, info.top, endTime, info.top, xloc.bar_time, extend.none, colorWithTransparency(breakerBlockColor, 0), line.style_dashed)
        ob.breakerBoxLineBottom := line.new(startTime, info.bottom, endTime, info.bottom, xloc.bar_time, extend.none, colorWithTransparency(breakerBlockColor, 0), line.style_dashed)


findOBSwings(len) =>
    var swingType = 0
    var obSwing top = obSwing.new(na, na)
    var obSwing bottom = obSwing.new(na, na)
    
    upper = ta.highest(len)
    lower = ta.lowest(len)

    swingType := high[len] > upper ? 0 : low[len] < lower ? 1 : swingType

    if swingType == 0 and swingType[1] != 0
        top := obSwing.new(bar_index[len], high[len], volume[len])
    
    if swingType == 1 and swingType[1] != 1
        bottom := obSwing.new(bar_index[len], low[len], volume[len])

    [top, bottom]

areaOfOB (orderBlockInfo OBInfoF) =>
    float XA1 = OBInfoF.startTime
    float XA2 = na(OBInfoF.breakTime) ? time + 1 : OBInfoF.breakTime
    float YA1 = OBInfoF.top
    float YA2 = OBInfoF.bottom
    float edge1 = math.sqrt((XA2 - XA1) * (XA2 - XA1) + (YA2 - YA2) * (YA2 - YA2))
    float edge2 = math.sqrt((XA2 - XA2) * (XA2 - XA2) + (YA2 - YA1) * (YA2 - YA1))
    float totalArea = edge1 * edge2
    totalArea

areaOfBB (orderBlockInfo OBInfoF) =>
    float XA1 = OBInfoF.breakTime
    float XA2 = time + 1
    float YA1 = OBInfoF.top
    float YA2 = OBInfoF.bottom
    float edge1 = math.sqrt((XA2 - XA1) * (XA2 - XA1) + (YA2 - YA2) * (YA2 - YA2))
    float edge2 = math.sqrt((XA2 - XA2) * (XA2 - XA2) + (YA2 - YA1) * (YA2 - YA1))
    float totalArea = edge1 * edge2
    totalArea

doOBsTouch (orderBlockInfo OBInfo1, orderBlockInfo OBInfo2) =>
    float XA1 = OBInfo1.startTime
    float XA2 = na(OBInfo1.breakTime) ? time + 1 : OBInfo1.breakTime
    float YA1 = OBInfo1.top
    float YA2 = OBInfo1.bottom

    float XB1 = OBInfo2.startTime
    float XB2 = na(OBInfo2.breakTime) ? time + 1 : OBInfo2.breakTime
    float YB1 = OBInfo2.top
    float YB2 = OBInfo2.bottom
    float intersectionArea = math.max(0, math.min(XA2, XB2) - math.max(XA1, XB1)) * math.max(0, math.min(YA1, YB1) - math.max(YA2, YB2))
    float unionArea = areaOfOB(OBInfo1) + areaOfOB(OBInfo2) - intersectionArea
    
    float overlapPercentage = (intersectionArea / unionArea) * 100.0

    if overlapPercentage > overlapThresholdPercentage
        true
    else
        false

isOBValid (orderBlockInfo OBInfo) =>
    valid = true
    if OBInfo.disabled
        valid := false
    valid

arrHasOB (orderBlock[] arr, orderBlock obF) =>
    hasOB = false
    if arr.size() > 0
        for i = 0 to arr.size() - 1
            orderBlock ob1 = arr.get(i)
            if doOBsTouch(ob1.info, obF.info)
                hasOB := true
                break
    hasOB

combineOBsFunc () =>
    if allOrderBlocksList.size() > 0
        lastCombinations = 999
        while lastCombinations > 0
            lastCombinations := 0
            for i = 0 to allOrderBlocksList.size() - 1
                curOB1 = allOrderBlocksList.get(i)
                for j = 0 to allOrderBlocksList.size() - 1
                    curOB2 = allOrderBlocksList.get(j)
                    if i == j
                        continue
                    if not isOBValid(curOB1.info) or not isOBValid(curOB2.info)
                        continue
                    if curOB1.info.obType != curOB2.info.obType
                        continue
                    if doOBsTouch(curOB1.info, curOB2.info)
                        curOB1.info.disabled := true
                        curOB2.info.disabled := true
                        orderBlock newOB = createOrderBlock(orderBlockInfo.new(math.max(curOB1.info.top, curOB2.info.top), math.min(curOB1.info.bottom, curOB2.info.bottom), curOB1.info.obVolume + curOB2.info.obVolume, curOB1.info.obType))
                        newOB.info.startTime := math.min(curOB1.info.startTime, curOB2.info.startTime)
                        newOB.info.breakTime := math.max(nz(curOB1.info.breakTime), nz(curOB2.info.breakTime))
                        newOB.info.breakTime := newOB.info.breakTime == 0 ? na : newOB.info.breakTime
                        newOB.info.timeframeStr := curOB1.info.timeframeStr

                        newOB.info.obVolume := curOB1.info.obVolume + curOB2.info.obVolume
                        newOB.info.obLowVolume := curOB1.info.obLowVolume + curOB2.info.obLowVolume
                        newOB.info.obHighVolume := curOB1.info.obHighVolume + curOB2.info.obHighVolume
                        newOB.info.bbVolume := nz(curOB1.info.bbVolume, 0) + nz(curOB2.info.bbVolume, 0)
                        newOB.info.breaker := curOB1.info.breaker or curOB2.info.breaker
                        
                        newOB.info.combined := true
                        if timeframe.in_seconds(curOB1.info.timeframeStr) != timeframe.in_seconds(curOB2.info.timeframeStr)
                            newOB.info.combinedTimeframesStr := (na(curOB1.info.combinedTimeframesStr) ? formatTimeframeString(curOB1.info.timeframeStr) : curOB1.info.combinedTimeframesStr) + " & " + (na(curOB2.info.combinedTimeframesStr) ? formatTimeframeString(curOB2.info.timeframeStr) : curOB2.info.combinedTimeframesStr)
                        allOrderBlocksList.unshift(newOB)
                        lastCombinations += 1
    true

handleOrderBlocksFinal () =>
    if DEBUG
        log.info("Bullish OB Count " + str.tostring(bullishOrderBlocksList.size()))
        log.info("Bearish OB Count " + str.tostring(bearishOrderBlocksList.size()))

    newBullishOBAlert = false
    newBearishOBAlert = false

    newBullishBBAlert = false
    newBearishBBAlert = false
    
    alertTimeOB = ""
    alertTimeBB = ""

    orderBlock[] orderBlocksToAdd = array.new<orderBlock>(0)

    for i = 0 to timeframeInfos.size() - 1
        curTimeframe = timeframeInfos.get(i)
        if not curTimeframe.isEnabled
            continue
        if not na(curTimeframe.bullishOrderBlocksList)
            if curTimeframe.bullishOrderBlocksList.size() > 0
                for j = 0 to math.min(curTimeframe.bullishOrderBlocksList.size() - 1, bullishOrderBlocks - 1)
                    orderBlockInfoF = curTimeframe.bullishOrderBlocksList.get(j)
                    orderBlockInfoF.timeframeStr := curTimeframe.timeframeStr
                    orderBlocksToAdd.unshift(createOrderBlock(orderBlockInfo.copy(orderBlockInfoF)))

        if not na(curTimeframe.bullishOrderBlocksList)
            if curTimeframe.bearishOrderBlocksList.size() > 0
                for j = 0 to math.min(curTimeframe.bearishOrderBlocksList.size() - 1, bearishOrderBlocks - 1)
                    orderBlockInfoF = curTimeframe.bearishOrderBlocksList.get(j)
                    orderBlockInfoF.timeframeStr := curTimeframe.timeframeStr
                    orderBlocksToAdd.unshift(createOrderBlock(orderBlockInfo.copy(orderBlockInfoF)))

    // Check New Order & Breaker Blocks
    if orderBlocksToAdd.size () > 0
        for i = 0 to orderBlocksToAdd.size() - 1
            obToTest = orderBlocksToAdd.get(i)
            if obToTest.info.breaker == false
                if not arrHasOB(allOrderBlocksList, obToTest)
                    alertTimeOB := obToTest.info.timeframeStr
                    if obToTest.info.obType == "Bull"
                        newBullishOBAlert := true
                    else
                        newBearishOBAlert := true
            else
                if not arrHasOB(allOrderBlocksList, obToTest)
                    alertTimeBB := obToTest.info.timeframeStr
                    if obToTest.info.obType == "Bull"
                        newBearishBBAlert := true
                    else
                        newBullishBBAlert := true
    
    // Delete Old Order Blocks
    if allOrderBlocksList.size () > 0
        for i = 0 to allOrderBlocksList.size() - 1
            safeDeleteOrderBlock(allOrderBlocksList.get(i))
    allOrderBlocksList.clear()

    // Add New Order Blocks
    if orderBlocksToAdd.size () > 0
        for i = 0 to orderBlocksToAdd.size() - 1
            allOrderBlocksList.unshift(orderBlocksToAdd.get(i))

    if combineOBs
        combineOBsFunc()

    if allOrderBlocksList.size() > 0
        for i = 0 to allOrderBlocksList.size() - 1
            curOB = allOrderBlocksList.get(i)
            if isOBValid(curOB.info) and dbgShowBBFVG
                renderOrderBlock(curOB, na)
    
    [alertTimeOB, alertTimeBB, newBullishOBAlert, newBearishOBAlert, newBullishBBAlert, newBearishBBAlert]

//#region Find Order Blocks

[top, btm] = findOBSwings(swingLength)
useBody = false
max = useBody ? math.max(close, open) : high
min = useBody ? math.min(close, open) : low

boxBtmBull = max[1]
boxTopBull = min[1]
boxLocBull = time[1]

for i = 1 to (bar_index - top.x) - 1
    boxBtmBull := math.min(min[i], boxBtmBull)
    boxTopBull := boxBtmBull == min[i] ? max[i] : boxTopBull
    boxLocBull := boxBtmBull == min[i] ? time[i] : boxLocBull

boxBtmBear = min[1]
boxTopBear = max[1]
boxLocBear = time[1]

for i = 1 to (bar_index - btm.x) - 1
    boxTopBear := math.max(max[i], boxTopBear)
    boxBtmBear := boxTopBear == max[i] ? min[i] : boxBtmBear
    boxLocBear := boxTopBear == max[i] ? time[i] : boxLocBear

if bar_index > last_bar_index - maxDistanceToLastBar
    // Bullish Order Block
    bullishBreaked = 0

    if bullishOrderBlocksList.size() > 0
        for i = bullishOrderBlocksList.size() - 1 to 0
            currentOB = bullishOrderBlocksList.get(i)
        
            if not currentOB.breaker 
                if (obEndMethod == "Wick" ? low : math.min(open, close)) < currentOB.bottom
                    currentOB.breaker := true
                    currentOB.breakTime := time
                    currentOB.bbVolume := volume
            else
                if (bbEndMethod == "Wick" ? high : close) > currentOB.top
                    bullishOrderBlocksList.remove(i)
                else if i < bullishOrderBlocks and top.y < currentOB.top and top.y > currentOB.bottom 
                    bullishBreaked := 1

    if close > top.y and not top.crossed
        top.crossed := true

        newOrderBlockInfo = orderBlockInfo.new(boxTopBull, boxBtmBull, volume + volume[1] + volume[2], "Bull", boxLocBull)
        newOrderBlockInfo.obLowVolume := volume[2]
        newOrderBlockInfo.obHighVolume := volume + volume[1]

        obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
        if obSize <= atr * maxATRMult
            bullishOrderBlocksList.unshift(newOrderBlockInfo)
            if bullishOrderBlocksList.size() > maxOrderBlocks
                bullishOrderBlocksList.pop()

    // Bearish Order Block

    bearishBreaked = 0

    if bearishOrderBlocksList.size() > 0
        for i = bearishOrderBlocksList.size() - 1 to 0
            currentOB = bearishOrderBlocksList.get(i)

            if not currentOB.breaker 
                if (obEndMethod == "Wick" ? high : math.max(open, close)) > currentOB.top
                    currentOB.breaker := true
                    currentOB.breakTime := time
                    currentOB.bbVolume := volume
            else
                if (bbEndMethod == "Wick" ? low : close) < currentOB.bottom
                    bearishOrderBlocksList.remove(i)
                else if i < bearishOrderBlocks and btm.y > currentOB.bottom and btm.y < currentOB.top 
                    bearishBreaked := 1

    if close < btm.y and not btm.crossed
        btm.crossed := true

        newOrderBlockInfo = orderBlockInfo.new(boxTopBear, boxBtmBear, volume + volume[1] + volume[2], "Bear", boxLocBear)
        newOrderBlockInfo.obLowVolume := volume + volume[1]
        newOrderBlockInfo.obHighVolume := volume[2]
        obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
        if obSize <= atr * maxATRMult
            bearishOrderBlocksList.unshift(newOrderBlockInfo)
            if bearishOrderBlocksList.size() > maxOrderBlocks
                bearishOrderBlocksList.pop()
//#endregion
//#endregion

//#region FVGs
arrHasFVG (FVG[] arr, FVG fvgF) =>
    hasFVG = false
    if arr.size() > 0
        for i = 0 to arr.size() - 1
            FVG fvg1 = arr.get(i)
            if fvg1.info.startTime == fvgF.info.startTime
                hasFVG := true
                break
    hasFVG

arrHasIFVG (FVG[] arr, FVG fvgF) =>
    hasIFVG = false
    if arr.size() > 0
        for i = 0 to arr.size() - 1
            FVG fvg1 = arr.get(i)
            if fvg1.info.isInverse
                if fvg1.info.startTime == fvgF.info.startTime
                    hasIFVG := true
                    break
    hasIFVG

renderFVG (FVG fvg, int customEndTime) =>
    fvg.isRendered := true
    if fvgEnabled and ((not ifvgEnabled) or (not fvg.info.isInverse)) and (showInvalidated or na(fvg.info.endTime))
        fvg.fvgBox := createFVGBox((fvg.info.combined and changeCombinedFVGsColor) ? combinedColor : fvg.info.isBull ? highColor : lowColor, 1.5)
        fvg.fvgBoxText := createFVGBox(color.new(color.white, 100))
        if fvgVolumetricInfo
            fvg.fvgBoxPositive := createFVGBox(highColor)
            fvg.fvgBoxNegative := createFVGBox(lowColor)
            fvg.fvgSeperator := line.new(na,na,na,na,xloc.bar_time,extend.none,textColor,line.style_dashed,1)
            fvg.fvgTextSeperator := line.new(na,na,na,na,xloc.bar_time,extend.none,textColor,line.style_solid,1)

        zoneSize = extendZonesDynamic ? na(fvg.info.endTime) ? extendZonesByTime : (fvg.info.endTime - fvg.info.startTime) : extendZonesByTime
        if na(fvg.info.endTime) and fvg.info.extendInfinite
            zoneSize := (time + 1) - fvg.info.startTime
        
        if not na(customEndTime)
            zoneSize := customEndTime - fvg.info.startTime

        startX = volumeBarsLeftSide ? fvg.info.startTime : fvg.info.startTime + zoneSize - zoneSize / 3
        maxEndX = volumeBarsLeftSide ? fvg.info.startTime + zoneSize / 3 : fvg.info.startTime + zoneSize

        moveBox(fvg.fvgBox, fvg.info.startTime, fvg.info.max, fvg.info.startTime + zoneSize, fvg.info.min)
        moveBox(fvg.fvgBoxText, volumeBarsLeftSide ? maxEndX : fvg.info.startTime, fvg.info.max, volumeBarsLeftSide ? fvg.info.startTime + zoneSize : startX, fvg.info.min)

        percentage = int((math.min(fvg.info.highVolume, fvg.info.lowVolume) / math.max(fvg.info.highVolume, fvg.info.lowVolume)) * 100.0)
        //FVGText = (na(fvg.info.combinedTimeframesStr) ? formatTimeframeString(fvg.info.timeframeStr) : fvg.info.combinedTimeframesStr) + " FVG"
        FVGText = ""
        box.set_text(fvg.fvgBoxText, (fvgVolumetricInfo ? str.tostring(fvg.info.totalVolume, format.volume) + " (" + str.tostring(percentage) + "%)\n" : "") + (combinedText and fvg.info.combined ? "[Combined]\n" : "") + FVGText)
        
        if fvg.info.combined and not changeCombinedFVGsColor
            fvg.fvgBox.set_bgcolor(colorWithTransparency(fvg.info.isBull ? highColor : lowColor, 1.1))

        if fvgVolumetricInfo
            showHighLowBoxText = false

            curEndXHigh = int(math.ceil((fvg.info.highVolume / fvg.info.totalVolume) * (maxEndX - startX) + startX))
            curEndXLow = int(math.ceil((fvg.info.lowVolume / fvg.info.totalVolume) * (maxEndX - startX) + startX))

            moveBox(fvg.fvgBoxPositive, mirrorVolumeBars ? startX : curEndXLow, fvg.info.max, mirrorVolumeBars ? curEndXHigh : maxEndX, (fvg.info.min + fvg.info.max) / 2)
            box.set_text(fvg.fvgBoxPositive, showHighLowBoxText ? str.tostring(fvg.info.highVolume, format.volume) : "")

            moveBox(fvg.fvgBoxNegative, mirrorVolumeBars ? startX : curEndXHigh, fvg.info.min, mirrorVolumeBars ? curEndXLow : maxEndX, (fvg.info.min + fvg.info.max) / 2)
            box.set_text(fvg.fvgBoxNegative, showHighLowBoxText ? str.tostring(fvg.info.lowVolume, format.volume) : "")

            moveLine(fvg.fvgSeperator, volumeBarsLeftSide ? startX : maxEndX, (fvg.info.min + fvg.info.max) / 2, volumeBarsLeftSide ? maxEndX : startX)

            line.set_xy1(fvg.fvgTextSeperator, volumeBarsLeftSide ? maxEndX : startX, fvg.info.max)
            line.set_xy2(fvg.fvgTextSeperator, volumeBarsLeftSide ? maxEndX : startX, fvg.info.min)
    
    // IFVG
    if fvg.info.isInverse and ifvgEnabled
        inverseColor = fvg.info.isBull ? bearishInverseColor : bullishInverseColor
        fvg.ifvgBox := createFVGBox(inverseColor)
        startTimeIFVG = ifvgFull ? fvg.info.startTime : fvg.info.endTime
        inverseZoneSize = na(fvg.info.inverseEndTime) ? ((time + 1) - startTimeIFVG) : (fvg.info.inverseEndTime - startTimeIFVG)
        moveBox(fvg.ifvgBox, startTimeIFVG, fvg.info.max, startTimeIFVG + inverseZoneSize, fvg.info.min)
        IFVGText = (na(fvg.info.combinedTimeframesStr) ? formatTimeframeString(fvg.info.timeframeStr) : fvg.info.combinedTimeframesStr) + " IFVG"
        box.set_text(fvg.ifvgBox, (ifvgVolumetricInfo ? str.tostring(fvg.info.inverseVolume, format.volume) + "\n" : "") + (combinedText and fvg.info.combined ? "[Combined]\n" : "") + IFVGText)


areaOfFVG (FVGInfo FVGInfoF) =>
    float XA1 = FVGInfoF.startTime
    float XA2 = na(FVGInfoF.endTime) ? time + 1 : FVGInfoF.endTime
    float YA1 = FVGInfoF.max
    float YA2 = FVGInfoF.min
    float edge1 = math.sqrt((XA2 - XA1) * (XA2 - XA1) + (YA2 - YA2) * (YA2 - YA2))
    float edge2 = math.sqrt((XA2 - XA2) * (XA2 - XA2) + (YA2 - YA1) * (YA2 - YA1))
    float totalArea = edge1 * edge2
    totalArea

doFVGsTouch (FVGInfo FVGInfo1, FVGInfo FVGInfo2) =>
    float XA1 = FVGInfo1.startTime
    float XA2 = na(FVGInfo1.endTime) ? time + 1 : FVGInfo1.endTime
    float YA1 = FVGInfo1.max
    float YA2 = FVGInfo1.min

    float XB1 = FVGInfo2.startTime
    float XB2 = na(FVGInfo2.endTime) ? time + 1 : FVGInfo2.endTime
    float YB1 = FVGInfo2.max
    float YB2 = FVGInfo2.min
    float intersectionArea = math.max(0, math.min(XA2, XB2) - math.max(XA1, XB1)) * math.max(0, math.min(YA1, YB1) - math.max(YA2, YB2))
    float unionArea = areaOfFVG(FVGInfo1) + areaOfFVG(FVGInfo2) - intersectionArea
    
    float overlapPercentage = (intersectionArea / unionArea) * 100.0

    //if FVGInfo1.isBull and str.contains(str.tostring(FVGInfo1.totalVolume, format.volume),"137M") and str.contains(str.tostring(FVGInfo2.totalVolume, format.volume),"221M")
    //    log.info(str.tostring(XA2) + " | " + str.tostring(XB2))

    if overlapPercentage > overlapThresholdPercentage
        true
    else
        false

isFVGValid (FVGInfo FVGInfoF) =>
    valid = true
    if (not showInvalidated) and (not na(FVGInfoF.endTime))
        valid := false
    else if FVGInfoF.disabled
        valid := false
    valid

isIFVGValid (FVGInfo FVGInfoF) =>
    valid = true
    if not ifvgEnabled
        valid := false
    else if (not showInvalidated) and (not na(FVGInfoF.inverseEndTime))
        valid := false
    else if FVGInfoF.disabled
        valid := false
    valid

isFVGValidInTimeframe (FVGInfo FVGInfoF) =>
    valid = true
    if (not showInvalidated) and (not na(FVGInfoF.endTime))
        valid := false
    else if FVGInfoF.disabled
        valid := false
    else if not na(FVGInfoF.endBarIndex) and (FVGInfoF.endBarIndex - FVGInfoF.startBarIndex) < minimumFVGSize
        valid := false
    else if na(FVGInfoF.endBarIndex) and deleteUntouched and (bar_index - FVGInfoF.lastTouched) > deleteUntouchedAfterXBars
        valid := false
    valid

isIFVGValidInTimeframe (FVGInfo FVGInfoF) =>
    valid = true
    if not ifvgEnabled
        valid := false
    else if (not showInvalidated) and (not na(FVGInfoF.inverseEndIndex))
        valid := false
    else if not na(FVGInfoF.inverseEndIndex) and (FVGInfoF.inverseEndIndex - FVGInfoF.endBarIndex) < minimumIFVGSize
        valid := false
    else if na(FVGInfoF.inverseEndIndex) and deleteUntouched and (bar_index - FVGInfoF.lastTouchedIFVG) > deleteUntouchedAfterXBars
        valid := false
    valid

combineFVGsFunc () =>
    if allFVGList.size() > 0
        lastCombinations = 999
        while lastCombinations > 0
            lastCombinations := 0
            for i = 0 to allFVGList.size() - 1
                curFVG1 = allFVGList.get(i)
                for j = 0 to allFVGList.size() - 1
                    curFVG2 = allFVGList.get(j)
                    if i == j
                        continue
                    if not isFVGValid(curFVG1.info) or not isFVGValid(curFVG2.info)
                        continue
                    if curFVG1.info.isBull != curFVG2.info.isBull
                        continue
                    if doFVGsTouch(curFVG1.info, curFVG2.info)
                        curFVG1.info.disabled := true
                        curFVG2.info.disabled := true
                        FVG newFVG = createFVG(createFVGInfo(math.max(curFVG1.info.max, curFVG2.info.max), math.min(curFVG1.info.min, curFVG2.info.min), curFVG1.info.isBull, math.min(curFVG1.info.t, curFVG2.info.t), 0))
                        newFVG.info.startTime := math.min(curFVG1.info.startTime, curFVG2.info.startTime)
                        newFVG.info.startBarIndex := math.min(curFVG1.info.startBarIndex, curFVG2.info.startBarIndex)
                        newFVG.info.endTime := math.max(nz(curFVG1.info.endTime), nz(curFVG2.info.endTime))
                        newFVG.info.endTime := newFVG.info.endTime == 0 ? na : newFVG.info.endTime
                        newFVG.info.endBarIndex := math.max(nz(curFVG1.info.endBarIndex), nz(curFVG2.info.endBarIndex))
                        newFVG.info.endBarIndex := newFVG.info.endBarIndex == 0 ? na : newFVG.info.endBarIndex
                        newFVG.info.timeframeStr := curFVG1.info.timeframeStr
                        newFVG.info.extendInfinite := curFVG1.info.extendInfinite or curFVG2.info.extendInfinite

                        newFVG.info.totalVolume := curFVG1.info.totalVolume + curFVG2.info.totalVolume
                        newFVG.info.lowVolume := curFVG1.info.lowVolume + curFVG2.info.lowVolume
                        newFVG.info.highVolume := curFVG1.info.highVolume + curFVG2.info.highVolume
                        newFVG.info.lastTouched := math.max(curFVG1.info.lastTouched, curFVG2.info.lastTouched)
                        newFVG.info.lastTouchedIFVG := math.max(curFVG1.info.lastTouchedIFVG, curFVG2.info.lastTouchedIFVG)
                        // Combine IFVG
                        newFVG.info.inverseEndIndex := math.max(nz(curFVG1.info.inverseEndIndex), nz(curFVG2.info.inverseEndIndex))
                        newFVG.info.inverseEndIndex := newFVG.info.inverseEndIndex == 0 ? na : newFVG.info.inverseEndIndex

                        newFVG.info.inverseEndTime := math.max(nz(curFVG1.info.inverseEndTime), nz(curFVG2.info.inverseEndTime))
                        newFVG.info.inverseEndTime := newFVG.info.inverseEndTime == 0 ? na : newFVG.info.inverseEndTime
                        if curFVG1.info.isInverse or curFVG2.info.isInverse
                            newFVG.info.inverseVolume := nz(curFVG1.info.inverseVolume) + nz(curFVG2.info.inverseVolume)
                            newFVG.info.isInverse := true

                        newFVG.info.combined := true
                        if timeframe.in_seconds(curFVG1.info.timeframeStr) != timeframe.in_seconds(curFVG2.info.timeframeStr)
                            newFVG.info.combinedTimeframesStr := (na(curFVG1.info.combinedTimeframesStr) ? formatTimeframeString(curFVG1.info.timeframeStr) : curFVG1.info.combinedTimeframesStr) + " & " + (na(curFVG2.info.combinedTimeframesStr) ? formatTimeframeString(curFVG2.info.timeframeStr) : curFVG2.info.combinedTimeframesStr)
                        allFVGList.unshift(newFVG)
                        lastCombinations += 1

handleFVGsFinal () => 
    FVG[] newFVGsToAdd = array.new<FVG>(0)

    alertTimeFVG = ""
    newBullishFVGAlert = false
    newBearishFVGAlert = false

    alertTimeIFVG = ""
    newBullishIFVGAlert = false
    newBearishIFVGAlert = false

    // Add Timeframe FVGs
    for i = 0 to timeframeInfos.size() - 1
        curTimeframe = timeframeInfos.get(i)
        if not curTimeframe.isEnabled
            continue
        if not na(curTimeframe.FVGInfoList)
            if curTimeframe.FVGInfoList.size() > 0
                for j = 0 to curTimeframe.FVGInfoList.size() - 1
                    newFVG = createFVG(FVGInfo.copy(curTimeframe.FVGInfoList.get(j)))
                    newFVG.info.timeframeStr := curTimeframe.timeframeStr
                    newFVGsToAdd.unshift(newFVG)

    // Check New FVGs
    if newFVGsToAdd.size () > 0
        for i = 0 to newFVGsToAdd.size() - 1
            curFVG = newFVGsToAdd.get(i)
            if not arrHasFVG(allFVGList, curFVG)
                alertTimeFVG := curFVG.info.timeframeStr
                if curFVG.info.isBull
                    newBullishFVGAlert := true
                else
                    newBearishFVGAlert := true
            if curFVG.info.isInverse
                if not arrHasIFVG(allFVGList, curFVG)
                    alertTimeIFVG := curFVG.info.timeframeStr
                    if curFVG.info.isBull
                        newBullishIFVGAlert := true
                    else
                        newBearishIFVGAlert := true
    
    // Delete Old FVGs
    if allFVGList.size () > 0
        for i = 0 to allFVGList.size() - 1
            safeDeleteFVG(allFVGList.get(i))
    allFVGList.clear()

    // Add New FVGs
    if newFVGsToAdd.size () > 0
        for i = 0 to newFVGsToAdd.size() - 1
            allFVGList.unshift(newFVGsToAdd.get(i))

    totalFoundFVGs = allFVGList.size()
    
    // Combine FVGs
    if combineFVGs
        combineFVGsFunc()

    // Render FVGs
    renderedFVGCount = 0
    extendedLastXFVGsCount = 0
    if allFVGList.size() > 0
        for i = 0 to allFVGList.size() - 1
            curFVG = allFVGList.get(i)
            if not isFVGValid(curFVG.info) and (not isIFVGValid(curFVG.info))
                continue
            if extendLastFVGs and na(curFVG.info.endTime) and extendedLastXFVGsCount < extendLastXFVGsCount
                extendedLastXFVGsCount += 1
                curFVG.info.extendInfinite := true
            
            if dbgShowBBFVG
                if curFVG.isRendered
                    safeDeleteFVG(curFVG)
                renderedFVGCount += 1
                renderFVG(curFVG, na)
    
    if DEBUG
        log.info("Rendered " + str.tostring(renderedFVGCount) + " / " + str.tostring(totalFoundFVGs) + " FVGs.")
    
    [alertTimeFVG, newBullishFVGAlert, newBearishFVGAlert, alertTimeIFVG, newBullishIFVGAlert, newBearishIFVGAlert]

shortVol = ta.sma(volume, 5)
longVol = ta.sma(volume, 15)
if (bar_index > last_bar_index - maxDistanceToLastBar) and barstate.isconfirmed
    // Add Found FVG
    bearCondition = false
    bullCondition = false

    shortTerm = volCheck ? shortVol : 1
    longTerm = volCheck ? longVol : 0

    firstBarSize = math.max(open, close) - math.min(open, close)
    secondBarSize = math.max(open[1], close[1]) - math.min(open[1], close[1])
    thirdBarSize = math.max(open[2], close[2]) - math.min(open[2], close[2])
    barSizeSum = firstBarSize + secondBarSize + thirdBarSize

    barSizeCheck = true
    //if (secondBarSize < math.max(firstBarSize, thirdBarSize))
        //barSizeCheck := false

    fvgBarsCheck = false
    if fvgBars == "Same Type"
        if (open > close and open[1] > close[1] and open[2] > close[2]) or (open <= close and open[1] <= close[1] and open[2] <= close[2])
            fvgBarsCheck := true
    else
        fvgBarsCheck := true

    if fvgBarsCheck and barSizeCheck
        maxCODiff = math.max(math.abs(close[2] - open[1]), math.abs(close[1] - open))
        if fvgFilterMethod == "Average Range"
            bearCondition := ((not fvgSensEnabled) or (barSizeSum * fvgSensitivity > atr / 1.5)) and (allowGaps or (maxCODiff <= atr))
            bullCondition := ((not fvgSensEnabled) or (barSizeSum * fvgSensitivity > atr / 1.5)) and (allowGaps or (maxCODiff <= atr))
        else if fvgFilterMethod == "Volume Threshold"
            thresholdMultiplier = (volumeThresholdPercent / 100.0)
            bearCondition := shortTerm > longTerm * thresholdMultiplier and (allowGaps or (maxCODiff <= atr))
            bullCondition := shortTerm > longTerm * thresholdMultiplier and (allowGaps or (maxCODiff <= atr))

    bearFVG = high < low[2] and close[1] < low[2] and bearCondition
    bullFVG = low > high[2] and close[1] > high[2] and bullCondition

    volSum3 = math.sum(volume, 3)
    float totalVolume = volCheck ? volSum3 : 0
    FVGInfo newFVGInfo = bearFVG ? createFVGInfo(low[2], high, false, time, totalVolume) : bullFVG ? createFVGInfo(low, high[2], true, time, totalVolume) : na
    FVGSize = bearFVG ? math.abs(low[2] - high) : bullFVG ? math.abs(low - high[2]) : 0

    FVGSizeEnough = (FVGSize * fvgSensitivity > atr)
    if FVGSizeEnough
        if not na(newFVGInfo)
            newFVGInfo.startTime := (startZoneFrom == "First Bar" ? time[2] : time)
            newFVGInfo.startBarIndex := bar_index - (startZoneFrom == "First Bar" ? 2 : 0)
            newFVGInfo.lastTouched := bar_index
            if bearFVG
                newFVGInfo.lowVolume := volCheck ? (volume + volume[1]) : 0
                newFVGInfo.highVolume := volCheck ? volume[2] : 0
            else
                newFVGInfo.lowVolume := volCheck ? volume[2] : 0
                newFVGInfo.highVolume := volCheck ? (volume + volume[1]) : 0

        if not na(newFVGInfo)
            FVGInfoList.unshift(newFVGInfo)
            while FVGInfoList.size() > showLastXFVGs
                FVGInfoList.pop()

    // Find Closed FVGs
    if FVGInfoList.size () > 0
        for i = 0 to FVGInfoList.size() - 1
            curFVG = FVGInfoList.get(i)
            // Is Touched FVG
            if ((curFVG.isBull) and low <= curFVG.max) or ((not curFVG.isBull) and high >= curFVG.min)
                curFVG.lastTouched := bar_index
            
            if ((not curFVG.isBull) and low <= curFVG.max) or ((curFVG.isBull) and high >= curFVG.min)
                curFVG.lastTouchedIFVG := bar_index  

            // IFVG Close
            if curFVG.isInverse and na(curFVG.inverseEndIndex)
                if (not curFVG.isBull) and (ifvgEndMethod == "Wick" ? low < curFVG.min : close < curFVG.min)
                    curFVG.inverseEndIndex := bar_index
                    curFVG.inverseEndTime := time
                if curFVG.isBull and (ifvgEndMethod == "Wick" ? high > curFVG.max : close > curFVG.max)
                    curFVG.inverseEndIndex := bar_index
                    curFVG.inverseEndTime := time
            
            if na(curFVG.endBarIndex)                
                // FVG End
                if curFVG.isBull and (fvgEndMethod == "Wick" ? (low < curFVG.min) : (close < curFVG.min))
                    curFVG.endBarIndex := bar_index
                    curFVG.endTime := time
                    curFVG.isInverse := true
                    curFVG.inverseVolume := nz(volume)
                    curFVG.lastTouchedIFVG := bar_index
                    //newIFVGTick := true
                if (not curFVG.isBull) and (fvgEndMethod == "Wick" ? (high > curFVG.max) : (close > curFVG.max))
                    curFVG.endBarIndex := bar_index
                    curFVG.endTime := time
                    curFVG.isInverse := true
                    curFVG.inverseVolume := nz(volume)
                    curFVG.lastTouchedIFVG := bar_index
                    //newIFVGTick := true

    // Remove Old FVGs
    FVGInfostoRemove = array.new<int>(0)

    if FVGInfoList.size() > 0
        for i = 0 to FVGInfoList.size() - 1
            curIndex = FVGInfoList.size() - 1 - i
            curFVGInfo = FVGInfoList.get(curIndex)
            if (not curFVGInfo.isInverse) and not isFVGValidInTimeframe(curFVGInfo)
                FVGInfostoRemove.push(curIndex)
            if curFVGInfo.isInverse and not isIFVGValidInTimeframe(curFVGInfo)
                FVGInfostoRemove.push(curIndex)

    if FVGInfostoRemove.size () > 0
        for i = 0 to FVGInfostoRemove.size() - 1
            deleteIndex = FVGInfostoRemove.get(i)
            FVGInfoList.remove(deleteIndex)
//#endregion

//#region Setup
reqSeq (timeframeStr) =>
    [FVGInfoListF, bullishOrderBlocksListF, bearishOrderBlocksListF] = request.security(syminfo.tickerid, timeframeStr, [FVGInfoList, bullishOrderBlocksList, bearishOrderBlocksList])
    [FVGInfoListF, bullishOrderBlocksListF, bearishOrderBlocksListF]

getTFData (timeframeInfo timeframeInfoF, timeframeStr) =>
    if not isTimeframeLower(timeframeInfoF.timeframeStr, timeframe.period) and timeframeInfoF.isEnabled
        [FVGInfoListF, bullishOrderBlocksListF, bearishOrderBlocksListF] = reqSeq(timeframeStr)
        [FVGInfoListF, bullishOrderBlocksListF, bearishOrderBlocksListF]
    else
        [na, na, na]

handleTimeframeInfo (timeframeInfo timeframeInfoF, FVGInfoList, bullishOrderBlocksListF, bearishOrderBlocksListF) =>
    if not isTimeframeLower(timeframeInfoF.timeframeStr, timeframe.period) and timeframeInfoF.isEnabled
        timeframeInfoF.FVGInfoList := FVGInfoList
        timeframeInfoF.bullishOrderBlocksList := bullishOrderBlocksListF
        timeframeInfoF.bearishOrderBlocksList := bearishOrderBlocksListF

[FVGInfoListTimeframe1, bullishOrderBlocksListTimeframe1, bearishOrderBlocksListTimeframe1] = getTFData(timeframeInfos.get(0), "")

if barstate.isconfirmed and (bar_index > last_bar_index - maxDistanceToLastBar)
    // Disable Duplicate Timeframes
    for i = 0 to timeframeInfos.size() - 1
        for j = 0 to timeframeInfos.size() - 1
            if i == j
                continue
            timeframeInfo1 = timeframeInfos.get(i)
            timeframeInfo2 = timeframeInfos.get(j)
            if timeframeInfo1.isEnabled and timeframeInfo2.isEnabled and timeframe.in_seconds(timeframeInfo1.timeframeStr) == timeframe.in_seconds(timeframeInfo2.timeframeStr)
                timeframeInfo1.isEnabled := false
    
    handleTimeframeInfo(timeframeInfos.get(0), FVGInfoListTimeframe1, bullishOrderBlocksListTimeframe1, bearishOrderBlocksListTimeframe1)
    
    [alertTimeFVG, newBullishFVGAlert, newBearishFVGAlert, alertTimeIFVG, newBullishIFVGAlert, newBearishIFVGAlert] = handleFVGsFinal()

    if OBsEnabled or BBsEnabled
        [alertTimeOB, alertTimeBB, newBullishOBAlert, newBearishOBAlert, newBullishBBAlert, newBearishBBAlert] = handleOrderBlocksFinal()
//#endregion

//#region ICT Unicorn

doBBFVGTouch (orderBlockInfo OBInfoX, FVGInfo FVGInfoX) =>
    if OBInfoX.breaker
        float XA1 = OBInfoX.breakTime
        float XA2 = time + 1
        float YA1 = OBInfoX.top
        float YA2 = OBInfoX.bottom

        float XB1 = FVGInfoX.startTime
        float XB2 = na(FVGInfoX.endTime) ? time + 1 : FVGInfoX.endTime
        float YB1 = FVGInfoX.max
        float YB2 = FVGInfoX.min
        float intersectionArea = math.max(0, math.min(XA2, XB2) - math.max(XA1, XB1)) * math.max(0, math.min(YA1, YB1) - math.max(YA2, YB2))
        float unionArea = areaOfBB(OBInfoX) + areaOfFVG(FVGInfoX) - intersectionArea
        
        float overlapPercentage = (intersectionArea / unionArea) * 100.0

        if overlapPercentage > overlapThresholdPercentage
            true
        else
            false
    else
        false

const int maxUnicorn = 75
const int atrLenUnicorn = 50
var initRun = true

buyAlertTick = false
sellAlertTick = false
tpAlertTick = false
slAlertTick = false

getPosition (positionText) =>
    if positionText == "Top Right"
        position.top_right
    else if positionText == "Top Center"
        position.top_center
    else if positionText == "Right Center"
        position.middle_right
    else if positionText == "Left Center"
        position.middle_left
    else if positionText == "Bottom Center"
        position.bottom_center
    else if positionText == "Middle Center"
        position.middle_center

type Unicorn
    string state
    int startTime

    string overlapDirection
    float retraceTo
    int enterPositionBar

    FVG fvg
    orderBlock OB

    float slTarget
    float tpTarget
    string entryType
    int entryTime
    int exitTime
    float entryPrice
    float exitPrice
    int dayEndedBeforeExit

var lineX = array.new<line>()
var boxX = array.new<box>()
var labelX = array.new<label>()

var Unicorn[] unicornList = array.new<Unicorn>(0)
var Unicorn lastUnicorn = na

atrUnicorn = ta.atr(atrLenUnicorn)

diffPercent (float val1, float val2) =>
    (math.abs(val1 - val2) / val2) * 100.0

highUnicornTPSL = ta.highest(unicornTPSLLength)
lowUnicornTPSL = ta.lowest(unicornTPSLLength)

//#region Unicorn
if bar_index > last_bar_index - maxDistanceToLastBar and barstate.isconfirmed
    if true
        // Find Session Start
        createNewUnicorn = true
        if not na(lastUnicorn)
            if na(lastUnicorn.exitPrice)
                createNewUnicorn := false // Don't enter if a trade is already entered
                
        if createNewUnicorn
            newUnicorn = Unicorn.new("Waiting For FVG-BB Overlap", time)
            unicornList.unshift(newUnicorn)
            lastUnicorn := newUnicorn
            log.info("New Unicorn")

        if not na(lastUnicorn)
            // Waiting For FVG-BB Overlap
            if lastUnicorn.state == "Waiting For FVG-BB Overlap"
                if allFVGList.size() > 0
                    for i = 0 to allFVGList.size() - 1
                        curFVG = allFVGList.get(i)
                        if allOrderBlocksList.size() > 0
                            for j = 0 to allOrderBlocksList.size() - 1
                                curOB = allOrderBlocksList.get(j)
                                if curOB.info.breaker
                                    if (curOB.info.obType == "Bear" and curFVG.info.isBull) or (curOB.info.obType == "Bull" and not curFVG.info.isBull)
                                        if doBBFVGTouch(curOB.info, curFVG.info) and (time == curFVG.info.startTime or time == curOB.info.breakTime)
                                            lastUnicorn.state := "FVG-BB Overlap"
                                            lastUnicorn.overlapDirection := (curFVG.info.isBull ? "Bull" : "Bear")
                                            lastUnicorn.retraceTo := curFVG.info.isBull ? curFVG.info.max : curFVG.info.min
                                            lastUnicorn.fvg := curFVG
                                            lastUnicorn.OB := curOB
            
            // FVG-BB Overlap
            if lastUnicorn.state == "FVG-BB Overlap"
                if not dbgRequireRetracement
                    lastUnicorn.state := "Enter Position"
                    lastUnicorn.enterPositionBar := bar_index
                else
                    lastUnicorn.state := "Require Retracement"
    // Require Retracement
    if not na(lastUnicorn)
        if lastUnicorn.state == "Require Retracement"
            if lastUnicorn.overlapDirection == "Bull" and low < lastUnicorn.retraceTo
                lastUnicorn.state := "Enter Position"
                lastUnicorn.enterPositionBar := bar_index
            if lastUnicorn.overlapDirection == "Bear" and high > lastUnicorn.retraceTo
                lastUnicorn.state := "Enter Position"
                lastUnicorn.enterPositionBar := bar_index

    // Enter Position
    if not na(lastUnicorn)
        if lastUnicorn.state == "Enter Position" and bar_index >= lastUnicorn.enterPositionBar + entryWaitBars
            lastUnicorn.state := "Entry Taken"
            lastUnicorn.entryTime := time
            lastUnicorn.entryPrice := close
            if lastUnicorn.overlapDirection == "Bull"
                lastUnicorn.entryType := "Long"
                buyAlertTick := true
                if tpslMethod == "Fixed"
                    lastUnicorn.slTarget := lastUnicorn.entryPrice * (1 - slPercent / 100.0)
                    lastUnicorn.tpTarget := lastUnicorn.entryPrice * (1 + tpPercent / 100.0)
                else if tpslMethod == "Dynamic"
                    lastUnicorn.slTarget := lastUnicorn.entryPrice - atrUnicorn * slATRMult
                    lastUnicorn.tpTarget := lastUnicorn.entryPrice + (math.abs(lastUnicorn.entryPrice - lastUnicorn.slTarget) * DynamicRR)
                else
                    lastUnicorn.slTarget := lowUnicornTPSL - atrUnicorn * dbgUnicornSLOffset
                    lastUnicorn.tpTarget := lastUnicorn.entryPrice + (math.abs(lastUnicorn.entryPrice - lastUnicorn.slTarget) * UnicornRR)
            else
                lastUnicorn.entryType := "Short"
                sellAlertTick := true
                if tpslMethod == "Fixed"
                    lastUnicorn.slTarget := lastUnicorn.entryPrice * (1 + slPercent / 100.0)
                    lastUnicorn.tpTarget := lastUnicorn.entryPrice * (1 - tpPercent / 100.0)
                else if tpslMethod == "Dynamic"
                    lastUnicorn.slTarget := lastUnicorn.entryPrice + atrUnicorn * slATRMult
                    lastUnicorn.tpTarget := lastUnicorn.entryPrice - (math.abs(lastUnicorn.entryPrice - lastUnicorn.slTarget) * DynamicRR)
                else
                    lastUnicorn.slTarget := highUnicornTPSL + atrUnicorn * dbgUnicornSLOffset
                    lastUnicorn.tpTarget := lastUnicorn.entryPrice - (math.abs(lastUnicorn.entryPrice - lastUnicorn.slTarget) * UnicornRR)
    // Entry Taken
    if not na(lastUnicorn)
        if lastUnicorn.state == "Entry Taken" and time > lastUnicorn.entryTime
            log.info("Entry Taken")
            if tpslMethod == "Fixed"
                // Take Profit
                if lastUnicorn.entryType == "Long" and ((high / lastUnicorn.entryPrice) - 1) * 100 >= tpPercent
                    tpAlertTick := true
                    lastUnicorn.exitPrice := lastUnicorn.entryPrice * (1 + tpPercent / 100.0)
                    lastUnicorn.exitTime := time
                    lastUnicorn.state := "Take Profit"
                if lastUnicorn.entryType == "Short" and ((low / lastUnicorn.entryPrice) - 1) * 100 <= -tpPercent
                    tpAlertTick := true
                    lastUnicorn.exitPrice := lastUnicorn.entryPrice * (1 - tpPercent / 100.0)
                    lastUnicorn.exitTime := time
                    lastUnicorn.state := "Take Profit"
                
                // Stop Loss
                if lastUnicorn.entryType == "Long" and ((low / lastUnicorn.entryPrice) - 1) * 100 <= -slPercent
                    slAlertTick := true
                    lastUnicorn.exitPrice := lastUnicorn.entryPrice * (1 - slPercent / 100.0)
                    lastUnicorn.exitTime := time
                    lastUnicorn.state := "Stop Loss"
                if lastUnicorn.entryType == "Short" and ((high / lastUnicorn.entryPrice) - 1) * 100 >= slPercent
                    slAlertTick := true
                    lastUnicorn.exitPrice := lastUnicorn.entryPrice * (1 + slPercent / 100.0)
                    lastUnicorn.exitTime := time
                    lastUnicorn.state := "Stop Loss"
            else
                // Take Profit
                if lastUnicorn.entryType == "Long" and high >= lastUnicorn.tpTarget
                    tpAlertTick := true
                    lastUnicorn.exitPrice := lastUnicorn.tpTarget
                    lastUnicorn.exitTime := time
                    lastUnicorn.state := "Take Profit"
                if lastUnicorn.entryType == "Short" and low <= lastUnicorn.tpTarget
                    tpAlertTick := true
                    lastUnicorn.exitPrice := lastUnicorn.tpTarget
                    lastUnicorn.exitTime := time
                    lastUnicorn.state := "Take Profit"
                
                // Stop Loss
                if lastUnicorn.entryType == "Long" and low <= lastUnicorn.slTarget
                    slAlertTick := true
                    lastUnicorn.exitPrice := lastUnicorn.slTarget
                    lastUnicorn.exitTime := time
                    lastUnicorn.state := "Stop Loss"
                if lastUnicorn.entryType == "Short" and high >= lastUnicorn.slTarget
                    slAlertTick := true
                    lastUnicorn.exitPrice := lastUnicorn.slTarget
                    lastUnicorn.exitTime := time
                    lastUnicorn.state := "Stop Loss"
//#endregion

//#region Render Unicorn

renderTopSL = false
renderBottomSL = false
renderTopTP = false
renderBottomTP = false

if not na(lastUnicorn)
    if lastUnicorn.state == "Stop Loss" and time >= lastUnicorn.exitTime
        if lastUnicorn.entryType == "Long"
            renderBottomSL := true
        else
            renderTopSL := true
        lastUnicorn.state := "Done"
    if lastUnicorn.state == "Take Profit"
        if lastUnicorn.entryType == "Long"
            renderTopTP := true
        else
            renderBottomTP := true
        lastUnicorn.state := "Done"

plotshape(renderTopSL, "", shape.circle, location.abovebar, color.red, textcolor = textColor, text = "SL", size = size.tiny)
plotshape(renderBottomSL, "", shape.circle, location.belowbar, color.red, textcolor = textColor, text = "SL", size = size.tiny)
plotshape(renderTopTP, "", shape.xcross, location.abovebar, color.blue, textcolor = textColor, text = "TP", size = size.tiny)
plotshape(renderBottomTP, "", shape.xcross, location.belowbar, color.blue, textcolor = textColor, text = "TP", size = size.tiny)

//#endregion

//#region Alerts
if barstate.islastconfirmedhistory
    initRun := false

alertcondition(buyAlertTick and not initRun, "Buy Signal", "")
alertcondition(sellAlertTick and not initRun, "Sell Signal", "")
alertcondition(tpAlertTick and not initRun, "Take-Profit Signal", "")
alertcondition(slAlertTick and not initRun, "Stop-Loss Signal", "")

if not initRun
    if buyAlertTick and buyAlertEnabled
        alert("Buy Signal")
    if sellAlertTick and sellAlertEnabled
        alert("Sell Signal")
    
    if tpAlertTick and tpAlertEnabled
        alert("Take-Profit Signal")
    if slAlertTick and slAlertEnabled
        alert("Stop-Loss Signal")

//#endregion

//#region Backtesting Dashboard

if barstate.islast and backtestDisplayEnabled
    var table backtestDisplay = table.new(getPosition(backtestingLocation), 2, 10, bgcolor = screenerColor, frame_width = 2, frame_color = color.black, border_width = 1, border_color = color.black)
    
    float totalTSProfitPercent = 0
    int successfulTrades = 0
    int unsuccessfulTrades = 0

    if unicornList.size() > 0
        for i = 0 to unicornList.size() - 1
            curUnicorn = unicornList.get(i)
            if not na(curUnicorn.entryPrice)
                isSuccess = false
                if not na(curUnicorn.exitPrice)
                    if (curUnicorn.entryType == "Long" and curUnicorn.exitPrice > curUnicorn.entryPrice) or (curUnicorn.entryType == "Short" and curUnicorn.exitPrice < curUnicorn.entryPrice)
                        totalTSProfitPercent += math.abs(diffPercent(curUnicorn.entryPrice, curUnicorn.exitPrice))
                        isSuccess := true
                    else
                        totalTSProfitPercent -= math.abs(diffPercent(curUnicorn.entryPrice, curUnicorn.exitPrice))
                        isSuccess := false

                if isSuccess
                    successfulTrades += 1
                else
                    unsuccessfulTrades += 1
    
    // Header
    table.merge_cells(backtestDisplay, 0, 0, 1, 0)
    table.cell(backtestDisplay, 0, 0, "Unicorn Backtesting", text_color = color.white, bgcolor = screenerColor)

    // Total ORBs
    table.cell(backtestDisplay, 0, 1, "Total Entries", text_color = color.white, bgcolor = screenerColor)
    table.cell(backtestDisplay, 1, 1, str.tostring(successfulTrades + unsuccessfulTrades), text_color = color.white, bgcolor = screenerColor)

    // Wins
    table.cell(backtestDisplay, 0, 2, "Wins", text_color = color.white, bgcolor = screenerColor)
    table.cell(backtestDisplay, 1, 2, str.tostring(successfulTrades), text_color = color.white, bgcolor = screenerColor)

    // Losses
    table.cell(backtestDisplay, 0, 3, "Losses", text_color = color.white, bgcolor = screenerColor)
    table.cell(backtestDisplay, 1, 3, str.tostring(unsuccessfulTrades), text_color = color.white, bgcolor = screenerColor)

    // Winrate
    table.cell(backtestDisplay, 0, 4, "Winrate", text_color = color.white, bgcolor = screenerColor)
    table.cell(backtestDisplay, 1, 4, str.tostring(100.0 * (successfulTrades / (successfulTrades + unsuccessfulTrades)), "#.##") + "%", text_color = color.white, bgcolor = screenerColor)

    // Average Profit %
    table.cell(backtestDisplay, 0, 5, "Average Profit", text_color = color.white, bgcolor = screenerColor)
    table.cell(backtestDisplay, 1, 5, str.tostring(totalTSProfitPercent / (successfulTrades + unsuccessfulTrades), "#.##") + "%", text_color = color.white, bgcolor = screenerColor)

    // Total Profit %
    table.cell(backtestDisplay, 0, 6, "Total Profit", text_color = color.white, bgcolor = screenerColor)
    table.cell(backtestDisplay, 1, 6, str.tostring(totalTSProfitPercent, "#.##") + "%", text_color = color.white, bgcolor = screenerColor)

//#endregion

if barstate.isconfirmed
    if lineX.size() > 0
        for i = 0 to lineX.size() - 1
            line.delete(lineX.get(i))

    if boxX.size() > 0
        for i = 0 to boxX.size() - 1
            box.delete(boxX.get(i))
    
    if labelX.size() > 0
        for i = 0 to labelX.size() - 1
            label.delete(labelX.get(i))

    lineX.clear()
    boxX.clear()
    labelX.clear()
    
    if unicornList.size() > 0
        for i = 0 to math.min(maxUnicorn, unicornList.size() - 1)
            curTS = unicornList.get(i)

            // TP / SL
            if not na(curTS.entryTime)
                // FVG & OB
                if showBB
                    safeDeleteOrderBlock(curTS.OB)
                    renderOrderBlock(curTS.OB, curTS.entryTime)
                if showFVG
                    safeDeleteFVG(curTS.fvg)
                    renderFVG(curTS.fvg, curTS.entryTime)

                // Entry Label
                if curTS.entryType == "Long"
                    labelX.push(label.new(curTS.entryTime, close, "Buy", xloc = xloc.bar_time, yloc = yloc.belowbar, textcolor = color.new(textColor, 0) , color = highColor, style = label.style_label_up, size = lblSize))
                else
                    labelX.push(label.new(curTS.entryTime, close, "Sell", xloc = xloc.bar_time, yloc = yloc.abovebar, textcolor = color.new(textColor, 0), color = lowColor, style = label.style_label_down, size = lblSize))
                
                if showTPSL
                    if dbgTPSLVersion == "Alternative"
                        offset = atrUnicorn / 3.0
                        endTime = nz(curTS.exitTime, time("", -15))
                        boxX.push(box.new(curTS.entryTime, curTS.tpTarget + offset, endTime, curTS.tpTarget - offset, text = "TAKE PROFIT (" + str.tostring(curTS.tpTarget, format.mintick) + ")", text_color = textColor, xloc = xloc.bar_time, border_width = 0, bgcolor = color.new(highColor, 50), text_size = size.small))
                        boxX.push(box.new(curTS.entryTime, curTS.slTarget + offset, endTime, curTS.slTarget - offset, text = "STOP LOSS (" + str.tostring(curTS.slTarget, format.mintick) + ")", text_color = textColor, xloc = xloc.bar_time, border_width = 0, bgcolor = color.new(lowColor, 50) , text_size = size.small))
                    else if dbgTPSLVersion == "Default"
                        endTime = nz(curTS.exitTime, time("", -15))
                        lineX.push(line.new(curTS.entryTime, curTS.entryPrice, curTS.entryTime, curTS.tpTarget, xloc = xloc.bar_time, color = highColor, style = line.style_dashed))
                        lineX.push(line.new(curTS.entryTime, curTS.tpTarget, endTime, curTS.tpTarget, xloc = xloc.bar_time, color = highColor, style = line.style_dashed))
                        labelX.push(label.new(endTime, curTS.tpTarget, "TP", xloc = xloc.bar_time, yloc = yloc.price, textcolor = color.new(textColor, 0), color = color.new(highColor, 50), style = label.style_label_left, size = lblSize))
                        //
                        lineX.push(line.new(curTS.entryTime, curTS.entryPrice, curTS.entryTime, curTS.slTarget, xloc = xloc.bar_time, color = lowColor, style = line.style_dashed))
                        lineX.push(line.new(curTS.entryTime, curTS.slTarget, endTime, curTS.slTarget, xloc = xloc.bar_time, color = lowColor, style = line.style_dashed))
                        labelX.push(label.new(endTime, curTS.slTarget, "SL", xloc = xloc.bar_time, yloc = yloc.price, textcolor = color.new(textColor, 0), color = color.new(lowColor, 50), style = label.style_label_left, size = lblSize))

            if not na(curTS.dayEndedBeforeExit)
                labelX.push(label.new(curTS.dayEndedBeforeExit, close, "Exit", xloc = xloc.bar_time, yloc = yloc.belowbar, textcolor = textColor, color = color.yellow, style = label.style_circle, size = size.tiny))

//#endregion